<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>SPX Report</title>
    <link rel="stylesheet" href="?SPX_UI_URI=/css/main.css">
</head>
<body>
    <div id="init-report">
        <div>
            <h2>Initializing...</h2>
            <p></p>
            <div class="progress">
                <div style="width: 0%"></div>
            </div>
            <p></p>
        </div>
    </div>

    <div style="display: flex; flex-direction: column; height: 100%">
        <div style="flex: 0 0 24px; display: flex; flex-direction: row;">
            <div id="metric-selector" class="widget" style="flex: 0 0 120px;">
                <select></select>
            </div>
            <div id="colorscheme-manager" class="widget" style="flex: 0 0 180px;"></div>
            <div id="category-legend" class="widget"></div>
            <div id="search-container" class="widget" style="flex: 0 0 195px;">
                <input type="text"
                       id="search_query"
                       name="search_query"
                       autocomplete="off"
                       placeholder="Search by function name">
                <button id="search_query_button_clear" onclick="$(window).trigger('spx-search-clear')" style="display: none;">
                    X
                </button>
            </div>
            <div id="color-scale" class="widget" style="flex: 1 1 150px;"></div>
        </div>
        <div id="overview" class="widget visualization" style="flex: 0 0 80px"></div>
        <div style="flex:1; min-height: 0; display: flex; flex-direction: column;">
            <div id="timeline" class="widget visualization" style="flex: 0 0 auto; height: 300px"></div>
            <div
                style="flex: 0 0 3px; width: 100%; background-color: #444; cursor: row-resize;"
                data-layout-splitter-target="timeline"
                data-layout-splitter-axis="y"
                data-layout-splitter-dir="-1"
                data-layout-splitter-min="30"
            ></div>
            <div style="flex: 1; min-height: 0; display: flex; flex-direction: row;">
                <div id="flatprofile" class="widget" style="flex: 0 0 auto; width: 900px"></div>
                <div
                    style="flex: 0 0 3px; height: 100%; background-color: #444; cursor: col-resize;"
                    data-layout-splitter-target="flatprofile"
                    data-layout-splitter-axis="x"
                    data-layout-splitter-dir="-1"
                    data-layout-splitter-min="500"
                ></div>
                <div id="flamegraph" class="widget visualization" style="flex: 1 1 auto"></div>
            </div>
        </div>
    </div>

    <!-- Required workaround for Firefox to fix a "no credentials" issue -->
    <script crossorigin src="?SPX_UI_URI=/js/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="></script>
    <script crossorigin src="?SPX_UI_URI=/js/jscolor.min.js" integrity="sha256-CJWfUCeP3jLdUMVNUll6yQx37gh9AKmXTRxvRf7jzro="></script>
    <script type="module" crossorigin src="?SPX_UI_URI=/js/fmt.js"></script>
    <script type="module" crossorigin src="?SPX_UI_URI=/js/profileData.js"></script>
    <script type="module" crossorigin src="?SPX_UI_URI=/js/widget.js"></script>
    <script type="module" crossorigin src="?SPX_UI_URI=/js/layoutSplitter.js"></script>

    <script type="module" crossorigin>
        function getImportUrl(path) {
            const rootUrl = new URL(import.meta.url);
            rootUrl.searchParams.set('SPX_UI_URI', path);
            return rootUrl.toString();
        }

        const fmt = await import(getImportUrl('/js/fmt.js'));
        const {ProfileDataBuilder} = await import(getImportUrl('/js/profileData.js'));
        const widget = await import(getImportUrl('/js/widget.js'));
        const layoutSplitter = await import(getImportUrl('/js/layoutSplitter.js'));

        $(() => {
            const m = window.location.href.match(/\bkey=([^&]+)/);
            if (!m) {
                return;
            }

            const key = m[1];
            let profileDataBuilder = null;

            const initDialog = {
                container: $('#init-report'),
                title: document.querySelector('#init-report p:nth-of-type(1)'),
                progressInfo: document.querySelector('#init-report p:nth-of-type(2)'),
                progress: document.querySelector('#init-report .progress'),
                progressBar: document.querySelector('#init-report .progress > div'),
                data: {}
            };

            function setProgress(title, current, total) {
                initDialog.title.innerHTML = title;

                if (!total) {
                    initDialog.progress.style.display = 'none';

                    return;
                }

                if (!(title in initDialog.data)) {
                    initDialog.data[title] = {
                        startCount: current,
                        startTime: new Date().getTime() / 1000,
                    };
                }

                initDialog.progress.style.display = 'block';
                initDialog.progressBar.style.width = (
                    Math.round(100 * current / total) + '%'
                );

                const currentTime = new Date().getTime() / 1000;

                const rate = (current - initDialog.data[title].startCount) / (currentTime - initDialog.data[title].startTime);
                const remainingTime = Math.round(
                    (total - current) / rate
                );

                initDialog.progressInfo.innerText = (
                    `${fmt.pct(current / total)} (${fmt.quantity(current)}, ${fmt.quantity(rate)}/s) ETA: ${remainingTime}s`
                );
            }

            fetch('?SPX_UI_URI=/data/metrics', {credentials: "same-origin"})
                .then(response => response.json())
                .then(response => {
                    profileDataBuilder = new ProfileDataBuilder(response.results);

                    return fetch('?SPX_UI_URI=/data/reports/metadata/' + key, {credentials: "same-origin"});
                })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Cannot load report metadata');
                    }

                    return response.json();
                })
                .then(metadata => {
                    profileDataBuilder.setMetadata(metadata);

                    return fetch('?SPX_UI_URI=/data/reports/get/' + key, {credentials: "same-origin"});
                })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Cannot load report');
                    }

                    let type = null;
                    let currentFunctionIdx = 0;
                    let lastTruncatedLine = '';

                    const decoder = new TextDecoder('ascii');

                    function readBuffer(buffer) {
                        if (buffer.length == 0) {
                            return;
                        }

                        const lines = decoder.decode(buffer).split("\n");
                        lines[0] = lastTruncatedLine + lines[0];
                        lastTruncatedLine = lines.pop();

                        for (const line of lines) {
                            if (line == '[events]') {
                                type = 'event';

                                continue;
                            }

                            if (line == '[functions]') {
                                type = 'function';

                                continue;
                            }

                            if (type == 'event') {
                                profileDataBuilder.addEvent(
                                    line.split(' ').map(e => parseFloat(e))
                                );

                                continue;
                            }

                            if (type == 'function') {
                                profileDataBuilder.setFunctionName(
                                    currentFunctionIdx++,
                                    line
                                );

                                continue;
                            }
                        }

                        setProgress(
                            'Building call list... (1/2)<br>'
                                + `${fmt.quantity(profileDataBuilder.getTotalCallCount())} function calls`
                            ,
                            profileDataBuilder.getCurrentCallCount(),
                            profileDataBuilder.getTotalCallCount()
                        );
                    }

                    if (!response.body) {
                        return response.arrayBuffer().then(buffer => {
                            const blockSize = 256 * 1024;
                            const readBlocks = (offset, resolve) => {
                                if (buffer.byteLength - offset <= blockSize) {
                                    readBuffer(new Uint8Array(buffer, offset));

                                    return new Promise(() => resolve());
                                }

                                readBuffer(new Uint8Array(buffer, offset, blockSize));

                                setTimeout(() => readBlocks(offset + blockSize, resolve), 0);
                            }

                            return new Promise(resolve => {
                                setTimeout(() => readBlocks(0, resolve), 0);
                            });
                        });
                    }

                    const reader = response.body.getReader();
                    const streamHandler = status => {
                        if (status.value) {
                            readBuffer(status.value);
                        }

                        if (!status.done) {
                            return reader.read().then(streamHandler);
                        }

                        return new Promise(resolve => resolve());
                    }

                    return reader.read().then(streamHandler);
                })
                .then(() => profileDataBuilder.buildCallRangeTree(
                    (current, total) => setProgress(
                        `Building call range tree... (2/2)<br>${fmt.quantity(total)} function calls`,
                        current,
                        total
                    )
                ))
                .then(() => {
                    initDialog.title.innerText = 'Initializing widgets...';

                    return new Promise(resolve => resolve(profileDataBuilder.getProfileData()));
                })
                .then(profileData => {
                    const metricSelector = $('#metric-selector select');
                    for (const metric of profileData.getMetadata().enabled_metrics) {
                        metricSelector.append(
                            `<option value="${metric}">${profileData.getMetricInfo(metric).name}</option>`
                        );
                    }

                    const widgets = [
                        new widget.ColorScale($('#color-scale'), profileData),
                        new widget.CategoryLegend($('#category-legend'), profileData),
                        new widget.ColorSchemeManager($('#colorscheme-manager'), profileData),
                        new widget.OverView($('#overview'), profileData),
                        new widget.TimeLine($('#timeline'), profileData),
                        new widget.FlatProfile($('#flatprofile'), profileData),
                        new widget.FlameGraph($('#flamegraph'), profileData),
                    ];

                    widgets.forEach(widget => widget.render());

                    metricSelector.on('change', () => {
                        widgets.forEach(widget => widget.setCurrentMetric(metricSelector.val()));
                        widgets.forEach(widget => widget.repaint());
                    });

                    layoutSplitter.init();
                    layoutSplitter.change(() => {
                        widgets.forEach(widget => {
                            if (!["timeline", "flatprofile", "flamegraph"].includes(widget.container.attr("id"))) {
                                return;
                            }

                            widget.handleResize();
                        });
                    });

                    return new Promise(resolve => resolve());
                })
                .then(() => initDialog.container.fadeOut(500))
                .catch(e => {
                    initDialog.progressInfo.innerText = e;
                    console.error(e)
                })
            ;
        });
    </script>
</body>
</html>
